package com.powroznik.tracker.client.android;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;

public class TrackerService1 extends Service {
	public class TrackerService1Binder extends Binder {
		public TrackerService1 getService() {
			return TrackerService1.this;
		}
	}

	private Device device;

	private DeviceConfiguration deviceConfiguration;

	private DateFormat dateFormat = DateFormat.getTimeInstance();

	private boolean gettingDevice;

	private boolean gettingDeviceConfiguration;

	private List<Location> locations;

	private Object lock = new Object();

	private StringBuffer log;

	private String server;

	private ServiceConnection sc5 = new ServiceConnection() {
		public void onServiceConnected(ComponentName name, IBinder service) {
			Log.d("SC", "onServiceConnected");
			ts5 = ((TrackerService5.TrackerService5Binder) service)
					.getService();
		}

		public void onServiceDisconnected(ComponentName name) {
			Log.d("SC", "onServiceDisconnected");
			ts5 = null;
		}
	};

	private TrackerService5 ts5;

	private Location convert(android.location.Location location1, Long run) {
		Location location2 = new Location();
		location2.setAccuracy(location1.hasAccuracy() ? Double
				.valueOf(location1.getAccuracy()) : null);
		location2.setAltitude(location1.hasAltitude() ? location1.getAltitude()
				: null);
		location2.setBearing(location1.hasBearing() ? Double.valueOf(location1
				.getBearing()) : null);
		location2.setLatitude(location1.getLatitude());
		location2.setLongitude(location1.getLongitude());
		location2.setProvider(location1.getProvider());
		location2.setRun(run);
		location2.setSpeed(location1.hasSpeed() ? Double.valueOf(location1
				.getSpeed()) : null);
		location2.setTime(new Date());
		return location2;
	}

	private boolean equals(Object o1, Object o2) {
		if (o1 == null && o2 == null) {
			return true;
		}
		if (o1 == null || o2 == null) {
			return false;
		}
		return o1.equals(o2);
	}

	private boolean equalsIgnoreCase(String s1, String s2) {
		if (s1 == null && s2 == null) {
			return true;
		}
		if (s1 == null || s2 == null) {
			return false;
		}
		return s1.equalsIgnoreCase(s2);
	}

	public void flushLocations() {
		Log.d("TS1", "flushLocations");
		log("Flushing...");
		Thread thread = new Thread(new Runnable() {
			public void run() {
				synchronized (lock) {
					Log.d("TS1", "run (from flushLocations)");
					if (ts5 == null) {
						return;
					}
					prepareServer();
					if (server == null) {
						return;
					}
					if (getDevice(false) == null) {
						return;
					}
					// Flush the locations
					prepareLocations();
					if (locations != null && locations.size() != 0) {
						if (!ts5.saveLocations(server, getDevice(false),
								locations)) {
							return;
						}
						locations.clear();
						saveLocations();
					}
					// Check the configuration
					DeviceConfiguration deviceConfiguration1 = getDeviceConfiguration(false);
					if (deviceConfiguration1 == null) {
						return;
					}
					DeviceConfiguration deviceConfiguration2 = ts5
							.getDeviceConfiguration(server, getDevice(false));
					if (deviceConfiguration2 == null) {
						return;
					}
					if (TrackerService1.this.equals(
							deviceConfiguration1.getDelay1(),
							deviceConfiguration2.getDelay1())
							&& TrackerService1.this.equals(
									deviceConfiguration1.getDelay2(),
									deviceConfiguration2.getDelay2())
							&& TrackerService1.this.equals(
									deviceConfiguration1.getDelay3(),
									deviceConfiguration2.getDelay3())) {
						return;
					}
					deviceConfiguration = deviceConfiguration2;
					writeObject("deviceConfiguration.ser", deviceConfiguration);
					startService(new Intent(TrackerService1.this,
							TrackerService2.class));
				}
			}

		});
		thread.start();
	}

	public int getBufferSize() {
		Log.d("TS1", "getBufferSize");
		prepareLocations();
		return locations.size();
	}

	public Device getDevice(boolean wait) {
		try {
			Log.d("TS1", "getDevice");
			Log.d("TS1", "wait = " + wait);
			if (device != null) {
				return device;
			}
			device = (Device) readObject("device.ser");
			if (device != null) {
				return device;
			}
			if (!gettingDevice) {
				gettingDevice = true;
				Thread thread = new Thread(new Runnable() {
					public void run() {
						try {
							Log.d("TS1", "run (from getDevice)");
							prepareServer();
							if (server == null) {
								return;
							}
							device = ts5.registerNewDevice(server);
							writeObject("device.ser", device);
						} finally {
							gettingDevice = false;
						}
					}
				});
				thread.start();
				if (wait) {
					thread.join();
				}
			}
			return device;
		} catch (Throwable t) {
			Log.w("TS1", t);
			return null;
		}
	}

	public DeviceConfiguration getDeviceConfiguration(boolean wait) {
		try {
			Log.d("TS1", "getDeviceConfiguration (not the web service)");
			Log.d("TS1", "wait = " + wait);
			if (deviceConfiguration != null) {
				return deviceConfiguration;
			}
			deviceConfiguration = (DeviceConfiguration) readObject("deviceConfiguration.ser");
			if (deviceConfiguration != null) {
				return deviceConfiguration;
			}
			if (!gettingDeviceConfiguration) {
				gettingDeviceConfiguration = true;
				Thread thread = new Thread(new Runnable() {
					public void run() {
						try {
							Log.d("TS1", "run (from getDeviceConfiguration)");
							Device device = getDevice(true);
							if (device == null) {
								return;
							}
							prepareServer();
							if (server == null) {
								return;
							}
							deviceConfiguration = ts5.getDeviceConfiguration(
									server, device);
							writeObject("deviceConfiguration.ser",
									deviceConfiguration);
						} finally {
							gettingDeviceConfiguration = false;
						}
					}
				});
				thread.start();
				if (wait) {
					thread.join();
				}
			}
			return deviceConfiguration;
		} catch (Throwable t) {
			Log.w("TS1", t);
			return null;
		}
	}

	public StringBuffer getLog() {
		Log.d("TS1", "getLog");
		prepareLog();
		return log;
	}

	private void log(String s) {
		synchronized (lock) {
			Log.d("TS1", "log(String)");
			prepareLog();
			log.insert(0, "\n");
			log.insert(0, s);
			log.insert(0, ": ");
			log.insert(0, dateFormat.format(new Date()));
			if (log.length() > 1 * 1024) {
				log.setLength(1 * 1024);
			}
			writeObject("log.ser", log);
		}
	}

	@Override
	public IBinder onBind(Intent intent) {
		Log.d("TS1", "onBind");
		return new TrackerService1Binder();
	}

	@Override
	public void onCreate() {
		Log.d("TS1", "onCreate");
		if (!bindService(new Intent(this, TrackerService5.class), sc5,
				Context.BIND_AUTO_CREATE)) {
			throw new RuntimeException();
		}
	}

	@Override
	public void onDestroy() {
		Log.d("TS1", "onDestroy");
		unbindService(sc5);
	}

	private void prepareLocations() {
		synchronized (lock) {
			Log.d("TS1", "prepareLocations");
			if (locations != null) {
				return;
			}
			locations = (List<Location>) readObject("locations.ser");
			if (locations != null) {
				return;
			}
			locations = new ArrayList<Location>();
		}
	}

	private void prepareLog() {
		synchronized (lock) {
			Log.d("TS1", "prepareLog");
			if (log != null) {
				return;
			}
			log = (StringBuffer) readObject("log.ser");
			if (log != null) {
				return;
			}
			log = new StringBuffer();
		}
	}

	private void prepareServer() {
		Log.d("TS1", "prepareServer");
		if (server != null) {
			return;
		}
		if (ts5 == null) {
			log("TS5 not ready");
			return;
		}
		for (String server : Settings.servers) {
			String s = ts5.ping(server, "Hello, world");
			if (s != null) {
				log("Found server at " + server);
				this.server = server;
				return;
			}
		}
		log("Server not found");
	}

	private Object readObject(String name) {
		synchronized (lock) {
			try {
				Log.d("TS1", "readObject (name = " + name + ")");
				File file = new File(getFilesDir(), name);
				if (!file.exists()) {
					return null;
				}
				FileInputStream fis = new FileInputStream(file);
				ObjectInputStream ois = new ObjectInputStream(fis);
				Object o = ois.readObject();
				ois.close();
				fis.close();
				return o;
			} catch (Throwable t) {
				Log.w("TS1", t);
				return null;
			}
		}
	}

	private void saveLocations() {
		Log.d("TS1", "saveLocations");
		writeObject("locations.ser", locations);
	}

	private void setDeviceConfiguration(DeviceConfiguration deviceConfiguration) {
		this.deviceConfiguration = deviceConfiguration;
		writeObject("deviceConfiguration.ser", deviceConfiguration);
	}

	public void trackLocations() {
		Log.d("TS1", "trackLocations");
		log("Tracking...");
		Thread thread = new Thread(new Runnable() {
			public void run() {
				DeviceConfiguration deviceConfiguration = getDeviceConfiguration(true);
				if (deviceConfiguration == null) {
					Log.d("TS1", "deviceConfiguration == null");
					return;
				}

				Looper.prepare();

				final long run = (long) (1000000000 * Math.random());

				LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
				List<LocationListener> locationListeners = new ArrayList<LocationListener>();
				for (final String provider : locationManager.getAllProviders()) {
					if (equalsIgnoreCase(provider, "passive")) {
						continue;
					}
					LocationListener locationListener = new LocationListener() {
						public void onLocationChanged(
								android.location.Location location) {
							synchronized (lock) {
								Log.d("TS1", "onLocationChanged (provider = "
										+ provider + ")");
								Log.d("TS1", "location = " + location);
								log("Received location update from " + provider);
								prepareLocations();
								locations.add(convert(location, run));
								saveLocations();
							}
						}

						public void onProviderDisabled(String provider) {
							Log.d("TS1", "onProviderDisabled (provider = "
									+ provider + ")");
						}

						public void onProviderEnabled(String provider) {
							Log.d("TS1", "onProviderEnabled (provider = "
									+ provider + ")");
						}

						public void onStatusChanged(String provider,
								int status, Bundle extras) {
							Log.d("TS1", "onStatusChanged (provider = "
									+ provider + ")");
						}
					};
					Log.d("TS1", "Requesting location updates (provider = "
							+ provider + ")");
					locationManager.requestLocationUpdates(provider,
							Settings.minTime, Settings.minDistance,
							locationListener);
					locationListeners.add(locationListener);
				}

				final Looper looper = Looper.myLooper();

				Timer timer = new Timer();
				TimerTask timerTask = new TimerTask() {
					@Override
					public void run() {
						looper.quit();
					}
				};
				timer.schedule(timerTask,
						deviceConfiguration.getDelay1() * 1000);

				Log.d("TS1", "Looping");
				Looper.loop();

				Log.d("TS1", "Removing all updates");
				for (LocationListener locationListener : locationListeners) {
					locationManager.removeUpdates(locationListener);
				}
			}
		});
		thread.start();
	}

	private void writeObject(String name, Object o) {
		synchronized (lock) {
			try {
				Log.d("TS1", "writeObject (name = " + name + ")");
				File file = new File(getFilesDir(), name);
				FileOutputStream fos = new FileOutputStream(file);
				ObjectOutputStream oos = new ObjectOutputStream(fos);
				oos.writeObject(o);
				oos.close();
				fos.close();
			} catch (Throwable t) {
				Log.w("TS1", t);
				throw new RuntimeException(t);
			}
		}
	}
}
